import asyncio
import pytest
import pytest_asyncio
from typing import AsyncGenerator
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.pool import StaticPool

from app.core.database import Base, get_db
from app.main import app
from app.utils.redis_client import redis_client
from app.core.config import settings


class InMemoryRedis:
    """Simple async Redis replacement for tests."""

    def __init__(self):
        self._strings = {}
        self._hashes = {}
        self._sets = {}

    async def get(self, key: str):
        return self._strings.get(key)

    async def set(self, key: str, value: str, ex: int = None):
        self._strings[key] = value
        return True

    async def delete(self, key: str):
        existed = int(key in self._strings or key in self._hashes or key in self._sets)
        self._strings.pop(key, None)
        self._hashes.pop(key, None)
        self._sets.pop(key, None)
        return existed

    async def exists(self, key: str):
        return int(key in self._strings or key in self._hashes or key in self._sets)

    async def sadd(self, key: str, *values: str):
        stored = self._sets.setdefault(key, set())
        before = len(stored)
        for value in values:
            stored.add(value)
        return len(stored) - before

    async def sismember(self, key: str, value: str):
        return value in self._sets.get(key, set())

    async def hset(self, key: str, mapping: dict):
        bucket = self._hashes.setdefault(key, {})
        added = 0
        for field, value in mapping.items():
            if field not in bucket:
                added += 1
            bucket[field] = value
        return added

    async def hget(self, key: str, field: str):
        return self._hashes.get(key, {}).get(field)

    async def hgetall(self, key: str):
        return dict(self._hashes.get(key, {}))

    async def hdel(self, key: str, *fields: str):
        bucket = self._hashes.get(key)
        if not bucket:
            return 0
        removed = 0
        for field in fields:
            if field in bucket:
                del bucket[field]
                removed += 1
        if not bucket:
            self._hashes.pop(key, None)
        return removed

    async def expire(self, key: str, seconds: int):
        return True

    async def flushdb(self):
        self._strings.clear()
        self._hashes.clear()
        self._sets.clear()

    async def close(self):
        return True

    async def ping(self):
        return True


# Test database URL (in-memory SQLite for testing)
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"

# Create test engine
test_engine = create_async_engine(
    TEST_DATABASE_URL,
    connect_args={"check_same_thread": False},
    poolclass=StaticPool,
)

# Create test session maker
TestAsyncSession = async_sessionmaker(
    test_engine,
    class_=AsyncSession,
    expire_on_commit=False,
)


@pytest_asyncio.fixture
async def db_session() -> AsyncGenerator[AsyncSession, None]:
    """Create a test database session"""
    async with test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    
    async with TestAsyncSession() as session:
        yield session


@pytest_asyncio.fixture
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
    """Create a test client with database dependency override"""
    
    async def override_get_db():
        yield db_session
    
    app.dependency_overrides[get_db] = override_get_db
    
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac
    
    app.dependency_overrides.clear()


@pytest.fixture(scope="session")
def event_loop():
    """Create an instance of the default event loop for the test session."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()


@pytest_asyncio.fixture(autouse=True)
async def setup_redis(monkeypatch):
    """Setup Redis for testing with in-memory stub"""

    mock_redis = InMemoryRedis()

    async def fake_init_redis_pool():
        redis_client.redis = mock_redis

    async def fake_close_redis_pool():
        if redis_client.redis:
            await redis_client.redis.close()
            redis_client.redis = None

    monkeypatch.setattr(redis_client, "init_redis_pool", fake_init_redis_pool)
    monkeypatch.setattr(redis_client, "close_redis_pool", fake_close_redis_pool)

    await redis_client.init_redis_pool()
    yield
    if redis_client.redis:
        await redis_client.redis.flushdb()  # Clear test data
        await redis_client.close_redis_pool()
